<?php
/**
 * This implementation of the decorator design pattern allows new behaviour to
 * be added to an object at run-time.
 * 
 * @see http://en.wikipedia.org/wiki/Decorator_pattern
 */
namespace TwinPeaks\Core;


class TDecoratorException extends \Exception
{
	const ERR_UNDEFINED_VARIABLE = 1;
	const ERR_UNDEFINED_METHOD = 2;
	const ERR_NO_DECORATION_TRAIT = 3;

	/**
	 * @return void
	 */
	function __construct($message, $code)
	{
		parent::__construct($message, $code);
	}
	
	/**
	 * @return void
	 */
	static function undefinedVariable($object, $varname)
	{
		throw new static(sprintf('Trying to access undefined variable %s::$%s',
			get_class($object), $varname), self::ERR_UNDEFINED_VARIABLE);
	}
	
	/**
	 * @void
	 */
	static function undefinedMethod($object, $methodName)
	{
		throw new static(sprintf('Trying to invoke undefined method %s::%s()',
			get_class($object), $methodName), self::ERR_UNDEFINED_METHOD);
	}
	
	/**
	 * @return void
	 */
	static function noDecorationTrait($object)
	{
		throw new static(sprintf('Object of the class %s must use TDecoration trait',
			get_class($object)), self::ERR_NO_DECORATION_TRAIT);		
	}
}

/**
 * Auxiliary trait. Must be used by classes serving as decorations.
 */
trait TDecoration
{
	/**
	 * @return mixed
	 */
	function getVariable($name)
	{
		return $this->{$name};
	}

	/**
	 * @return void
	 */
	function setVariable($name, $value)
	{
		$this->{$name} = $value;
	}

	/**
	 * @return boolean
	 */
	final function hasVariable($name)
	{
		static $objVars = null;

		if ($objVars === null)
			$objVars = array_keys(get_object_vars($this));

		return in_array($name, $objVars);
	}
	
	/**
	 * Exports class methods except for some.
	 * 
	 * @return array Array of class methods
	 */
	final function decorationExport()
	{
		$result = [];
	
		foreach (get_class_methods($this) as $methodName)
		{
			// Skip some methods.
			if ($methodName == '__construct' || $methodName == 'getVariable' ||
				$methodName == 'setVariable' || $methodName == 'hasVariable' ||
				$methodName == __METHOD__)
			{
				continue;
			}

			$closure = (new \ReflectionMethod($this, $methodName))->getClosure($this);
			$result[$methodName] = $closure;
		}

		return $result;
	}
}
/**
 * Use this trait to make a class decorable.
 */
trait TDecorator
{
	/**
	 * @var array Array of objects used for decoration.
	 */
	private $decoratorObjStack = [];
	
	/**
	 * @var array Array of new methods which were added
	 */
	private $decoratorBehaviourStack = [];
	
	
	/**
	 * @return mixed
	 */
	function __get($name)
	{
		$arrRef = &$this->decoratorObjStack;
		for ($n = count($arrRef) - 1; $n >= 0; $n--)
			if ($arrRef[$n]->hasVariable($name))
				return $arrRef[$n]->getVariable($name);

		if (static::onUnaccessibleThrowException())
			TDecoratorException::undefinedVariable($this, $name);
	}
	
	/**
	 * @return void
	 */
	function __set($name, $value)
	{
		foreach ($this->decoratorObjStack as $obj)
			if ($obj->hasVariable($name))
			{
				$obj->setVariable($name, $value);
				return;
			}
		
		if (static::onUnaccessibleThrowException())
			TDecoratorException::undefinedVariable($this, $name);
	}
	
	/**
	 * @return mixed
	 */
	function __call($name, $args)
	{
		// PHP method names are case insensitive so we must convert it to lowercase.
		$name = strtolower($name);
		$arrRef = &$this->decoratorBehaviourStack;
		if (isset($arrRef[$name]))
		{
			$result = [];
			for ($n = count($arrRef[$name]) - 1; $n >= 0; $n--)
				$result[] = call_user_func_array($arrRef[$name][$n], $args);
			if (count($result) == 1)
				return $result[0];
			else
				return $result;
		}
		
		if (static::onUnaccessibleThrowException())
			TDecoratorException::undefinedMethod($this, $name);
	}
	
	/**
	 * @return boolean
	 */
	static function onUnaccessibleThrowException()
	{
		return true;
	}
	
	/**
	 * Decorates object with the public methods of another one.
	 * 
	 * @param object $obj Object used as decoration 
	 * @final
	 * @return void
	 */
	final function decorateWithObject($obj)
	{
		// Check if object uses TDecoration trait.
		$classTraits = array_keys((new \ReflectionClass($obj))->getTraits());
		if (!in_array('TwinPeaks\Core\TDecoration', $classTraits))
			TDecoratorException::noDecorationTrait($obj);
		
		foreach ($obj->decorationExport() as $methodName => $closure)
		{
			$this->decorateWith($methodName, $closure);
		}
		
		$this->decoratorObjStack[] = $obj;
	}
	
	/**
	 * @final
	 * @return array
	 */
	final function renderDecoration()
	{
		$args = func_get_args();
		$methodName = array_shift($args);
		return $this->__call($methodName, $args);
	}
	
	/**
	 * Decorates object with a new method
	 * 
	 * @final
	 * @return boolean
	 */
	final function decorateWith($methodName, $closure)
	{
		$methodName = strtolower($methodName);
		$arrRef = &$this->decoratorBehaviourStack;
		if (!isset($arrRef[$methodName]))
			$arrRef[$methodName] = [];
		
		$arrRef[$methodName][] = $closure->bindTo($this,
			get_called_class());
	}

	/**
	 * @final
	 * @return boolean
	 */
	final function hasDecoration($methodName)
	{
		$arrRef = &$this->decoratorBehaviourStack;
		$methodName = strtolower($methodName);
		return isset($arrRef[$methodName]) && count($arrRef[$methodName]) ?
			true : false;
	}

	/**
	 * @final
	 * @return void
	 */
	final function removeDecoration($methodName)
	{
		unset($this->decoratorBehaviourStack[$methodName]);
	}
}
